/* ====================================================================
 * (#)Header: SortOrder.java
 * (#)Revision: 
 * (#)Date Created: Mar 2, 2007 12:07:22 AM
 * (#)Created By: Karol Wozniak
 * ====================================================================
 *
 * Copyright (c) 2007 karolak .  All rights reserved.
 *
 * ====================================================================
 * 
 * The software application represented here is the sole property of 
 * karolak. Its unauthorized utilization is expressly prohibited.
 * 
 * ====================================================================
 */
package com.karolak.xml;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * <code>XPathDao</code> helps to perfom xpaths queries and simplifies namespace
 * handling. <br />
 * 
 * @author <a href="mailto:kwozniak@hotmail.com">Karol Wozniak</a>
 */
public class XPathDao {

	private String xml;
	private Document document;
	private NamespaceContext context;

	// === generic api === //
	public Object select(String xPath, Node node, NamespaceContext context, QName type) {
		Object result = null;
		if (isEmpty(xPath) || node == null) {
			return result;
		}
		if (type == null) {
			type = XPathConstants.NODE;
		}
		XPathExpression xPathExpression = createXPathExpression(xPath, context);
		try {
			result = xPathExpression.evaluate(node, type);
		} catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
		return result;
	}

	// === query nodes api ===//
	public Node selectNode(String xPath) {
		return (Node) select(xPath, getDocument().getDocumentElement(), getContext(), XPathConstants.NODE);
	}

	public Node selectNode(String xPath, Node node) {
		return (Node) select(xPath, node, getContext(), XPathConstants.NODE);
	}

	public NodeList selectNodeList(String xPath) {
		return (NodeList) select(xPath, getDocument().getDocumentElement(), getContext(), XPathConstants.NODESET);
	}

	public NodeList selectNodeList(String xPath, Node node) {
		return (NodeList) select(xPath, node, getContext(), XPathConstants.NODESET);
	}

	// === query data types api === //
	public String selectString(String xPath) {
		return (String) select(xPath, getDocument().getDocumentElement(), getContext(), XPathConstants.STRING);

	}

	public Double selectDouble(String xPath) {
		Double value = (Double) select(xPath, getDocument().getDocumentElement(), getContext(), XPathConstants.NUMBER);
		if (Double.isNaN(value)) {
			return null;
		}
		return value;
	}

	public Boolean selectBoolean(String xPath) {
		Boolean value = (Boolean) select(xPath, getDocument().getDocumentElement(), getContext(), XPathConstants.BOOLEAN);
		return value;
	}

	public Date selectDate(String xPath) {
		Date date = null;
		String text = selectString(xPath);
		if (isEmpty(text)) {
			return date;
		}
		XMLGregorianCalendar calendar = XmlUtils.getInstance().toXmlCalendar(text);
		date = calendar.toGregorianCalendar().getTime();
		return date;
	}

	// === query typed collections === //
	public List<String> selectStrings(String xPath) {
		return selectStrings(xPath, getDocument().getDocumentElement());
	}

	public List<String> selectStrings(String xPath, Node currentNode) {
		List<String> result = new ArrayList<String>();
		if (isEmpty(xPath) || currentNode == null) {
			return result;
		}
		try {
			XPathExpression xPathExpression = createXPathExpression(xPath, getContext());
			NodeList nodeList = (NodeList) xPathExpression.evaluate(currentNode, XPathConstants.NODESET);
			if (nodeList == null) {
				return result;
			} else {
				for (int i = 0; i < nodeList.getLength(); i++) {
					result.add(nodeList.item(i).getTextContent());
				}
			}
		} catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
		return result;
	}

	public List<Double> selectDoubles(String xPath) {
		List<String> textList = selectStrings(xPath, getDocument().getDocumentElement());
		List<Double> doubleList = new ArrayList<Double>();
		if (textList == null || textList.isEmpty()) {
			return doubleList;
		}
		for (String str : textList) {
			Double d = null;
			if (!isEmpty(str)) {
				d = Double.valueOf(str);
			}
			doubleList.add(d);
		}
		return doubleList;
	}

	// ==== update/create api ===/
	public Node update(String xPath, String value) {
		Node node = null;
		if (isEmpty(xPath)) {
			return node;
		}
		node = selectNode(xPath);
		if (node == null) {
			return node;
		}
		node.setTextContent(value);
		return node;
	}

	// TODO
	public Node createNode(String xPath) {
		Node node = null;
		if (isEmpty(xPath)) {
			return node;
		}
		// node = selectNode(xPath);
		// if (node != null) {
		// return node;
		// }

		String[] tokens = xPath.split("/");

		if (tokens.length == 0) {
			return node;
		}

		Node parentNode = getDocument().getDocumentElement();
		for (int i = 0; i < tokens.length; i++) {
			node = selectNode(tokens[i], parentNode);
			if (node == null) {
				node = getDocument().createElement(tokens[i]);
				parentNode.appendChild(node);
			}
			parentNode = node;
		}
		return node;
	}

	// === factory methods === //
	public static XPathDao createDao(String xml) {
		XPathDao dao = new XPathDao();
		Document document = DomTransformer.getInstance().transform(xml, false);
		dao.setXml(xml);
		dao.setDocument(document);
		return dao;
	}

	public static XPathDao createDao(String xml, boolean nameSpaceAware) {
		XPathDao dao = new XPathDao();
		Document document = DomTransformer.getInstance().transform(xml, nameSpaceAware);
		dao.setXml(xml);
		dao.setDocument(document);
		if (nameSpaceAware) {
			NamespaceContext context = XmlUtils.getInstance().createNamespaceContext(document);
			dao.setContext(context);
		}
		return dao;
	}

	public static XPathDao createDao(String xml, boolean nameSpaceAware, Map<String, String> nameSpaces) {
		XPathDao dao = new XPathDao();
		Document document = DomTransformer.getInstance().transform(xml, nameSpaceAware);
		NamespaceContext context = XmlUtils.getInstance().createNamespaceContext(nameSpaces);
		dao.setXml(xml);
		dao.setDocument(document);
		dao.setContext(context);
		return dao;
	}

	// === utility api === //
	public String transformDocument() {
		return DomTransformer.getInstance().transform(getDocument());
	}

	protected XPathExpression createXPathExpression(String xPath) {
		return createXPathExpression(xPath, null);
	}

	protected XPathExpression createXPathExpression(String xPath, NamespaceContext context) {
		XPathExpression expression = null;
		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();
		if (context != null) {
			xpath.setNamespaceContext(context);
		}
		try {
			expression = xpath.compile(xPath);
		} catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
		return expression;
	}

	public String predicateXpath(String xPath) {
		if (isEmpty(xPath)) {
			return xPath;
		}
		if (xPath.startsWith("/") || xPath.startsWith(".") || xPath.startsWith("*")) {
			return xPath;
		}
		xPath = "//" + xPath;
		return xPath;
	}

	protected boolean isEmpty(String str) {
		return str == null || str.trim().length() == 0;
	}

	// === get/set === //
	public void setXml(String xml) {
		this.xml = xml;
	}

	public String getXml() {
		return xml;
	}

	public Document getDocument() {
		return document;
	}

	public void setDocument(Document document) {
		this.document = document;
	}

	public NamespaceContext getContext() {
		return context;
	}

	public void setContext(NamespaceContext context) {
		this.context = context;
	}
}
